Skip to content

InnoDB中 Buffer Pool 与磁盘一致性原理

在讨论 MySQL 缓存一致性时,很多人第一反应是 Redis 和数据库双写问题。但如果把视角下沉到 InnoDB 内部,会发现还有一个更底层、也更核心的一致性问题:

Buffer Pool 中的数据页和磁盘上的数据页,为什么可以长期不一致,而数据库依然能保证事务正确性?

这个问题如果只用“redo log 能恢复”来回答,其实是不够的。因为真正的关键不只是 redo log,而是 InnoDB 在 Buffer Poolredo log、脏页刷盘、checkpoint、崩溃恢复之间建立了一整套协作机制。本文从 MySQL 5.7/8.0 的通用实现思路出发,系统地拆开这个问题。

一、先说结论:InnoDB 追求的不是“实时物理一致”,而是“事务语义一致”

理解这套机制,第一步就是放弃一个直觉误区:

InnoDB 并不要求 Buffer Pool 和磁盘页在任意时刻都完全一致。

绝大多数时间里,它们本来就是不一致的:

  • Buffer Pool 里的页可能已经被修改过,是新版本
  • 磁盘上的页可能还是旧版本
  • 只要崩溃后能够恢复到正确状态,这种“不一致”就是被允许的

所以 InnoDB 保证的一致性,不是“内存页和磁盘页内容始终相同”,而是:

  • 已提交事务不能丢
  • 未提交事务不能污染最终结果
  • 宕机恢复后数据必须回到逻辑正确状态

这是一种以事务恢复能力为核心的一致性,而不是以“同步写盘”为核心的一致性。

二、Buffer Pool 到底是什么

Buffer Pool 是 InnoDB 最核心的内存结构之一,本质上是对磁盘页的缓存。InnoDB 的基本管理单位不是“行”,而是“页(page)”,默认页大小通常是 16KB。无论是查询、更新还是索引访问,最终大多都要落到页级别操作。

从源码抽象上看,Buffer Pool 里管理的不是单纯的一段页内存,而是一组带元信息的缓存块。你可以粗略理解为:

  • 页本身的数据内容
  • 页所属的表空间 ID、页号等标识
  • 是否是脏页
  • 是否被固定引用
  • 所在的链表位置
  • 与刷盘、淘汰、哈希定位相关的控制信息

在 5.7/8.0 的实现中,常见会接触到的概念包括:

  • buf_pool_t:Buffer Pool 实例本身
  • buf_block_t / buf_page_t:页对应的控制块
  • LRU、free、flush 等链表
  • page hash:用于根据 (space_id, page_no) 快速定位缓存页

也就是说,Buffer Pool 不是一个简单的“大数组”,而是一个带多种索引结构和状态管理机制的缓存子系统。

三、一次更新发生时,真正修改的是谁

假设执行这样一条语句:

sql
update user set name = 'A' where id = 1;

从 InnoDB 内部视角看,关键步骤大致是这样的:

  1. 根据索引定位目标记录所在页
  2. 如果该页不在 Buffer Pool,就从磁盘读入
  3. 对 Buffer Pool 中的页进行修改
  4. 生成对应的 undo 信息
  5. 生成对应的 redo 记录
  6. 页被标记为 dirty page
  7. 事务提交时先保证 redo 持久化
  8. 后台线程在未来某个时间点把脏页刷回磁盘

这里最关键的一点是:

真正被立即修改的是 Buffer Pool 中的页,而不是磁盘页。

这意味着事务执行过程中,最先变成“新版本”的地方是内存。磁盘页是否立即更新,并不是事务提交路径上必须同步完成的事情。

四、为什么不能每次都直接写磁盘

如果每次更新都要求同步改磁盘页,理论上当然最直观,但实际代价极高。

原因在于磁盘写存在两个问题:

  • 数据页写入通常是随机写
  • 随机写延迟高,吞吐差

而数据库事务更新往往非常频繁,如果每次都把 16KB 页立刻刷盘,性能会差到不可接受。

所以 InnoDB 采用了一个经典策略:

把随机的数据页写,转换成顺序的日志写。

也就是我们熟悉的 WALWrite-Ahead Logging,预写日志。它的核心约束是:

在对应数据页落盘之前,描述这次修改的 redo log 必须先安全落盘。

这就是 InnoDB 一致性的第一根支柱。

五、Redo Log:为什么它能兜住“内存已改、磁盘未改”的风险

redo log 的职责不是给人看,而是给机器恢复用。它记录的是“某个页上的某种物理修改”,因此常被称为物理日志或页级重做日志。

在一次更新中,只要内存页已经被改过,就必须生成对应的 redo 信息。事务提交时,系统首先确保这些 redo 已经持久化到 redo log file。此时即使数据页还没有刷回磁盘,事务也可以认为提交成功。

因为宕机之后还可以这样做:

  1. 读取 redo log
  2. 找到尚未反映到磁盘页上的那部分修改
  3. 重新应用这些修改
  4. 让磁盘页追上崩溃前已提交事务的状态

所以,redo log 的本质作用是:

把“修改结果暂时只存在于内存”的风险,转移为“修改过程已经被可靠记录在日志中”。

这样一来,Buffer Pool 和磁盘页就算短时间不一致,也不会破坏已提交事务的持久性。

六、脏页:InnoDB 明知页脏了,为什么还不立刻刷

当 Buffer Pool 中的页被修改后,这个页就成了 dirty page。脏页的含义很简单:

内存中的版本比磁盘中的版本新。

这时候很多初学者会问,既然已经知道它脏了,为什么不马上写回去?

答案是:因为“知道脏”和“必须马上刷”是两回事。

InnoDB 会把脏页统一管理起来,后续根据系统状态决定刷盘时机。常见触发因素包括:

  • redo log 空间压力变大
  • Buffer Pool 脏页比例过高
  • 后台 page cleaner 主动推进刷盘
  • checkpoint 需要前移
  • Buffer Pool 需要淘汰某个脏页
  • 正常关闭数据库

这种设计的意义在于,刷盘可以做成更平滑、更批量、更有调度感的过程,而不是把用户线程绑死在高成本随机 I/O 上。

所以从系统角度看,脏页并不是异常状态,而是 InnoDB 的常态。

七、Checkpoint:它决定了恢复从哪里开始

如果 redo log 能记录修改,那是不是把所有 redo 永久留着就行?当然不行。日志空间总是有限的,系统必须知道:

哪些 redo 已经“兑现”到磁盘页了,哪些还没有。

这就是 checkpoint 的意义。

你可以把 checkpoint 理解成一个恢复边界:

  • 在 checkpoint 之前的 redo,对应的数据页已经安全落盘
  • 在 checkpoint 之后的 redo,可能还需要用于崩溃恢复

因此,数据库重启时不需要从 redo 的起点全部重放,只需要从 checkpoint 之后开始处理即可。

checkpoint 的本质不是一个简单标记,而是 InnoDB 在“日志推进”和“脏页落盘”之间建立的协调点。它让系统能够回答两个关键问题:

  • 哪些日志已经没必要保留恢复意义
  • 崩溃恢复时从哪里开始是充分且必要的

所以如果说 redo log 解决的是“怎么恢复”,那么 checkpoint 解决的就是“恢复的起点在哪里”。

八、Undo Log:它不负责磁盘一致性,但它是事务一致性的一部分

讨论 Buffer Pool 和磁盘一致性时,经常会顺手把 undo log 也带上,但要注意它的职责边界。

undo log 主要解决两个问题:

  • 事务回滚
  • MVCC 读旧版本

它并不是用来处理“脏页尚未刷盘怎么办”的核心机制。这个问题主要由 redo + checkpoint + flush 体系解决。

但 undo 仍然是事务一致性的组成部分,因为数据库不仅要保证“崩溃后能恢复已提交事务”,还要保证“未提交事务能撤回,读视图能看到正确版本”。

所以从更完整的事务语义来看:

  • redo 负责“做过的怎么保住”
  • undo 负责“没做完的怎么撤回”

这两者共同服务于 InnoDB 的事务模型,只是它们解决的问题不同。

九、崩溃恢复时到底恢复什么

现在来看一个经典场景:

  • 某个事务已经修改了 Buffer Pool 中的数据页
  • 对应 redo log 已经落盘
  • 脏页还没来得及刷回磁盘
  • 机器宕机了

这时磁盘上的数据页显然还是旧的。如果没有 redo,这次提交过的事务就丢了。但因为 redo 已经在,所以重启时 InnoDB 可以执行 crash recovery。

从逻辑上看,恢复过程做的是两件事:

  1. 根据 checkpoint 找到需要处理的 redo 范围
  2. 把这些 redo 对应的修改重新作用到数据页上

这意味着恢复系统并不关心“宕机前那个内存页长什么样”,因为它已经丢了。恢复依赖的是磁盘页 + redo log 的组合。

这也是为什么说:

InnoDB 从来不试图保护内存本身,它保护的是“内存修改可重建的能力”。

这个设计非常关键。因为内存注定不可靠,而日志和磁盘页才是重建状态的基础。

十、把整条链路串起来看

到这里,可以把一次更新简化成一条完整链路:

  1. 数据页被读入 Buffer Pool
  2. 在 Buffer Pool 中修改页内容
  3. 生成 undo log,支持回滚和 MVCC
  4. 生成 redo log,记录页修改
  5. 页被标记为 dirty page
  6. 事务提交时,先保证 redo 持久化
  7. 用户收到提交成功
  8. 后台线程异步刷脏页
  9. checkpoint 随着刷盘推进
  10. 宕机时,checkpoint 之后的 redo 参与恢复

如果要用一句话概括这套机制,可以说:

InnoDB 用 redo log 承接提交时刻的持久性要求,用脏页刷盘完成最终物化,用 checkpoint 划定恢复边界。

这三者共同构成了 Buffer Pool 和磁盘之间的一致性解决方案。

十一、从源码视角看,应该关注哪些对象

如果想继续往源码层走,建议优先建立“模块感”,而不是一开始就陷入所有函数细节。

Buffer Pool 相关,可以重点看:

  • buf0buf.*
  • buf_pool_t
  • buf_block_t
  • LRU/free/flush 链表组织
  • page hash 的页定位逻辑

redo / log system 相关,可以重点看:

  • log0log.*
  • log_sys
  • mini-transaction(mtr)相关路径
  • redo 写入与刷盘接口

刷脏页和 checkpoint 相关,可以重点看:

  • page cleaner 线程
  • flush list 管理
  • checkpoint 推进逻辑

如果以“写请求链路”为主线去读源码,通常比按文件硬啃效率更高。因为源码本身是模块化的,但理解必须依赖调用链。

十二、一个容易混淆的点:Binlog 不负责这件事

很多人在讲 redo 时,会顺带把 binlog 拉进来,结果把问题讲混。

要明确一点:

Buffer Pool 和磁盘页的一致性,不是 binlog 保证的。

binlog 是 MySQL Server 层的逻辑日志,主要服务于:

  • 主从复制
  • 数据归档
  • 基于逻辑变更的恢复

而 Buffer Pool 与磁盘页之间的崩溃一致性,是 InnoDB 存储引擎内部通过 redo、flush、checkpoint 完成的。

当然,在事务提交阶段,redo 和 binlog 会通过两阶段提交协作,避免“存储引擎提交了但 binlog 没写”这类跨层不一致。但这已经是另一个问题了。它解决的是引擎层与 Server 层日志一致性,不是本文讨论的内存页与磁盘页一致性

十三、为什么这套设计是成立的

最后回到最初的问题:为什么 Buffer Pool 和磁盘可以不一致?

因为 InnoDB 从一开始就没有把“一致性”定义为“同一时刻值相等”,而是定义为:

  • 提交结果可持久
  • 崩溃后状态可恢复
  • 事务边界可保证
  • 未提交修改不会错误固化

只要 redo log 先于数据页持久化,只要 checkpoint 正确推进,只要脏页最终能刷盘,只要崩溃后可以重做,那么“内存新、磁盘旧”就只是系统运行过程中的一个中间态,而不是错误状态。

从工程角度看,这是一种非常经典的权衡:

  • 放弃实时物理一致
  • 换取高吞吐、高并发写入能力
  • 再通过日志和恢复机制补上正确性

这也是 InnoDB 作为事务型存储引擎能够同时兼顾性能和可靠性的根本原因。

结语

如果只记一句话,我建议记住这句:

MySQL InnoDB 并不要求 Buffer Pool 和磁盘页实时一致,它通过 WAL、redo log、脏页刷盘和 checkpoint 机制,保证事务提交后的状态在宕机后仍然可以被正确恢复。

这才是 InnoDB 缓存一致性的真正答案。

Last updated: